package com.nageffer.shortlink.project.service.Impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageffer.shortlink.project.common.constant.RedisKeyConstant;
import com.nageffer.shortlink.project.common.convention.exception.ClientException;
import com.nageffer.shortlink.project.common.convention.exception.ServiceException;
import com.nageffer.shortlink.project.common.enums.VailDateTypeEnum;
import com.nageffer.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.nageffer.shortlink.project.dao.entity.*;
import com.nageffer.shortlink.project.dao.mapper.*;
import com.nageffer.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.nageffer.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.nageffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageffer.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.nageffer.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.nageffer.shortlink.project.dto.resp.*;
import com.nageffer.shortlink.project.mq.producer.RabbitMqStatsProducer;
import com.nageffer.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.nageffer.shortlink.project.service.LinkStatsTodayService;
import com.nageffer.shortlink.project.service.ShortLinkService;
import com.nageffer.shortlink.project.statsStrategy.UseMqFactory;
import com.nageffer.shortlink.project.utils.HashUtil;
import com.nageffer.shortlink.project.utils.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.nageffer.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortlinkCreatePenetrationBloomFilter;

    private final ShortLinkGotoMapper shortLinkGotoMapper;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    private final LinkAccessStatsMapper linkAccessStatsMapper;

    private final LinkLocaleStatsMapper linkLocaleStatsMapper;

    private final LinkOsStatsMapper linkOsStatsMapper;

    private final LinkBrowserStatsMapper linkBrowserStatsMapper;

    private final LinkAccessLogsMapper linkAccessLogsMapper;

    private final LinkDeviceStatsMapper linkDeviceStatsMapper;

    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    private final LinkStatsTodayMapper linkStatsTodayMapper;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Value("${short-link.domian.default}")
    private String defaultDomain;

    private final LinkStatsTodayService linkStatsTodayService;

    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;

    private final RabbitMqStatsProducer rabbitMqStatsProducer;

    private final UseMqFactory useMqFactory;

    /**
     * 创建短链接
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {
        //验证短链接是否合法
        verificationWhitelist(shortLinkCreateReqDTO.getOriginUrl());

        //把原始链接根据hash算法，生成短链接后缀
        String suffix = generateSuffix(shortLinkCreateReqDTO);

        //复制属性到ShortLinkDO中，并手动赋值没有的属性
        ShortLinkDO shortLinkDO = BeanUtil.toBean(shortLinkCreateReqDTO, ShortLinkDO.class);
        shortLinkDO.setShortUri(suffix);
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setFavicon(getFavicon(shortLinkCreateReqDTO.getOriginUrl()));
        String fullShortUrl = defaultDomain + "/" + suffix;
        shortLinkDO.setFullShortUrl(fullShortUrl);
        shortLinkDO.setTotalPv(0);
        shortLinkDO.setTotalUv(0);
        shortLinkDO.setTotalUip(0);
        //创建短链接时，指定默认域名
        shortLinkDO.setDomain(defaultDomain);

        /*
        优化方法：创建短链接时顺便往路由表中添加信息，方便短链接跳转时拿到gid
         */
        ShortLinkGotoDO LinkGotoDO = ShortLinkGotoDO.builder()
                .gid(shortLinkCreateReqDTO.getGid())
                .fullShortUrl(fullShortUrl)
                .build();

        //新增短链接信息
        //数据库中的shortUri字段，编码改为utf8，否则大小写会被忽略。
        //并发情况下，对插入时的异常进行捕获，因为数据库中设置了唯一索引，短链接重复的话会出现重复键冲突
        try {
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(LinkGotoDO);
        } catch (DuplicateKeyException e) { //DuplicateKeyException：向数据库中插入数据时发生了重复键（唯一性约束）冲突
            //捕获异常是为了解决多线程情况下出现的问题。
            // throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));

            //如果重复键冲突了，有可能reids返回成功了，但是redis持久化失败了并且宕机了
            // 首先判断是否存在布隆过滤器，如果不存在直接新增
            if (!shortlinkCreatePenetrationBloomFilter.contains(fullShortUrl)) {
                shortlinkCreatePenetrationBloomFilter.add(fullShortUrl);
            }
            throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
        }

        //把插入成功的短链接放入布隆过滤器
        shortlinkCreatePenetrationBloomFilter.add(fullShortUrl);

        /*
        优化方法：对热点数据记进行缓存预热
         */
        stringRedisTemplate.opsForValue().set(
                String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                shortLinkDO.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(shortLinkCreateReqDTO.getValidDate()),//调用工具类设置默认时间
                TimeUnit.MILLISECONDS
        );

        //构建返回参数对象
        ShortLinkCreateRespDTO result = ShortLinkCreateRespDTO
                .builder()
                .gid(shortLinkCreateReqDTO.getGid())
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .originUrl(shortLinkCreateReqDTO.getOriginUrl())
                .build();
        return result;
    }

    /**
     * 优化：
     * 布隆过滤器特点：查询是否存在，如果返回存在，可能数据是不存在的。查询是否存在，如果返回不存在，数据一定不存在。
     * 因此去掉了插入数据库键重复查询数据库的业务，直接抛出异常。
     * 并发场景下会出现短链接生成重复：
     * 同一毫秒下，大量请求相同的原始链接会生成重复短链接，并判断不存在，通过该方式访问数据库。
     * 为此，使用 UUID 替换了当前时间戳，来一定程度减少重复的短链接生成报错。
     */

    //根据hash算法，生成短链接后缀
    public String generateSuffix(ShortLinkCreateReqDTO shortLinkCreateReqDTO){
        String suffix;
        int count= 0;
        while (true){
            //防止频繁生成重复的短链接，导致频繁访问数据库，造成数据库压力过大
            if (count > 10){
                throw new ServiceException("短链接生成过于频繁，请稍后充重试！");
            }
            //生成短链接后缀
            //使用System.currentTimeMillis()来保证生成suffix的随机性时，使用jmeter进行测试时会在一毫秒内生成同样的suffix，这里改成UUID
            suffix = HashUtil.hashToBase62(shortLinkCreateReqDTO.getOriginUrl() + UUID.randomUUID().toString());
            String fullShortUrl = defaultDomain + "/" + suffix;
            //使用布隆过滤器判断suffix是否重复
            if (!shortlinkCreatePenetrationBloomFilter.contains(fullShortUrl)){
                break;
            }
            count++;
        }
        return suffix;
    }

    /**
     * 分页查询短链接
     * 查询的是某个分组的所有短链接信息
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortlink(ShortLinkPageReqDTO shortLinkPageReqDTO) {
        //根据分组id分页查询
        /*
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, shortLinkPageReqDTO.getGid()) //根据分组id查询
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0)
                .orderByDesc(ShortLinkDO::getCreateTime);
        IPage<ShortLinkDO> page = baseMapper.selectPage(shortLinkPageReqDTO, queryWrapper);
        */

        //改为使用mybatis
        IPage<ShortLinkDO> page = baseMapper.pageLink(shortLinkPageReqDTO);
        //相当于对records属性重新赋值
        return page.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            return result;
        });
    }

    /**
     * 查询短链接分组内数量
     */
    @Override
    public List<ShortLinkGroupCountRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        //Wrappers.query()使用字符串作为查询条件，需要手动编写 SQL 语句
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");
        List<Map<String, Object>> mapList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(mapList, ShortLinkGroupCountRespDTO.class);
    }

    /**
     * 修改短链接
     * 根据fullShortUrl更新记录，fullShortUrl是唯一索引。
     * 修改：
     * 1、唯一索引改为fullShortUrl和delTime，防止新插入的短链接和被删除的出现冲突。
     * 2、对监控相关表进行数据迁移，否则短链接gid改变的话会找不到。
     * 3、引入读写锁。
     */
    @Transactional
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        //验证短链接是否合法
        verificationWhitelist(requestParam.getOriginUrl());

        //首先需要判断分组gid有没有改变，因为短链接表是根据gid作为分片键进行分表的。
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }

        //如果没有改变，更新原来的记录。
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    //如果有效期类型为永久，有效期为null；如果有效期类型为自定义，需要设置有效期时长
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .favicon(getFavicon(requestParam.getOriginUrl()))
                    .build();
            baseMapper.update(shortLinkDO, updateWrapper);

            //更新后删除相关的缓存
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
        } else {
            //引入读写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();
            // rLock.lock();
            if (!rLock.tryLock()) {
                throw new ServiceException("短链接正在被访问，请稍后再试...");
            }

            try {
                //如果改变了，直接删除原来的记录，重新插入新纪录。
                LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = new ShortLinkDO();
                delShortLinkDO.setDelTime(System.currentTimeMillis());//设置删除时的时间戳
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, updateWrapper);
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(defaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);

                //由于监控相关的表都是根据gid进行分组的，所以分组变了的话，监控相关表都要进行数据迁移
                //对今日统计表进行数据迁移
                LambdaQueryWrapper<LinkStatsTodayDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkStatsTodayDO.class)
                        .eq(LinkStatsTodayDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkStatsTodayDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkStatsTodayDO::getDelFlag, 0);
                List<LinkStatsTodayDO> linkStatsTodayDOList = linkStatsTodayMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    linkStatsTodayMapper.deleteBatchIds(linkStatsTodayDOList
                            .stream()
                            .map(LinkStatsTodayDO::getId)
                            .toList());
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid()));
                    linkStatsTodayService.saveBatch(linkStatsTodayDOList);
                }
                //对路由表进行数据迁移
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);
                //对基础访问监控表进行数据迁移
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);
                //对地区表进行数据迁移
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);
                //对操作系统表进行数据迁移
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);
                //对浏览器表进行数据迁移
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);
                //对设备表进行数据迁移
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);
                //对网络表进行数据迁移
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);
                //对日志表进行数据迁移
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
            } finally {
                rLock.unlock();
            }

            //更新后删除相关的缓存
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
        }
    }

    /**
     * 短链接跳转原始链接
     * 解决缓存击穿、缓存穿透问题
     * 判断短链接是否过期
     * 不存在的话跳转到自定义404页面
     */
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        //获取客户端发起请求的服务器名（即主机名）
        String serverName = request.getServerName();

        //拿到端口号，如果时默认的80就返回空
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(each -> ":" + each)
                .orElse("");

        //拿到短链接全路径
        String fullShortUrl = serverName + serverPort + "/" + shortUri;

        //检查缓存是是否存在fullShortUrl对应的OriginUrl，有的话直接跳转后返回
        String originLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originLink)){

            //对短链接跳转进行监控
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, null, statsRecord);

            ((HttpServletResponse)response).sendRedirect(originLink);
            return;
        }

        //缓存中不存在的话，使用布隆过滤器判断是否存在于数据库。避免redis和mysql数据不一致的情况
        boolean contains = shortlinkCreatePenetrationBloomFilter.contains(fullShortUrl);
        //如果不存在，直接返回；如果存在，再去查数据库
        if (!contains){
            ((HttpServletResponse)response).sendRedirect("/page/notfound"); //重定向到自定义的404页面
            return;
        }
        //判断缓存中是否是空值，"-"代表空
        String isNull = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        //如果是，直接返回；否则，继续查
        if (StrUtil.isNotBlank(isNull)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound"); //重定向到自定义的404页面
            return;
        }
        //后面查询数据库时，不管是不是空值，都缓存。

        //缓存中没有的话，加锁进行查询
        RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();//阻塞锁，trylock（）是非阻塞锁
        try {
            //进行双重检测，有可能其他线程拿到锁以后已经重建缓存了
            originLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originLink)){

                //对短链接跳转进行监控
                ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, null, statsRecord);

                ((HttpServletResponse)response).sendRedirect(originLink);
                return;
            }
            //进行双重检测，判断是不是其他线程先拿到锁后缓存空值了
            isNull = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(isNull)) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            //从路由表中根据fullShortUrl查到gid
            //由于gid是分片键，所以新建一个路由表来找到gid，否则查询可能会变得更加复杂和低效
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);

            //如果为空，缓存空值
            if (shortLinkGotoDO == null){
                stringRedisTemplate.opsForValue().set(
                        String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),
                        "-",
                        30,
                        TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound"); //重定向到自定义的404页面
                return;
            }

            //查询到要跳转的地址，并进行重定向
            LambdaQueryWrapper<ShortLinkDO> queryWrapper2 = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper2);

            /*
            优化方法：判断短链接是否过期，过期了缓存空值
            */

            Date validDate = shortLinkDO.getValidDate();
            if (shortLinkDO == null || (validDate != null && shortLinkDO.getValidDate().before(new Date()))){
                stringRedisTemplate.opsForValue().set(
                        String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),
                        "-",
                        30,
                        TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound"); //重定向到自定义的404页面
                return;
            }
            //把fullShortUrl对应的OriginUrl放到缓存中
            stringRedisTemplate.opsForValue().set(
                    String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),//调用工具类设置默认时间
                    TimeUnit.MILLISECONDS
            );

            //对短链接跳转进行监控
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, shortLinkDO.getGid(), statsRecord);

            //跳转到对应的url
            ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取网站对应的图标
     */
    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 短链接状态监控
     * 这里后面被重构了
     */
//    private void shortLinkStats(String fullShortUrl, String gid, ServletRequest request, ServletResponse response){
//        // TODO 监控时加入写锁，这里没写，后面重构的时候加上了。
//
//        AtomicBoolean uvFirstFlag = new AtomicBoolean();//该用户是否是第一次访问
//        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
//        AtomicReference<String> uv = new AtomicReference<>();//为了在后面可以拿到uv
//        try {
//            //统计uv
//            Runnable addResponseCookieTask = () -> {
//                String uvStr = UUID.fastUUID().toString();
//                uv.set(uvStr);
//                Cookie uvCookie = new Cookie("uv", uvStr);
//                uvCookie.setMaxAge(60 * 60 * 24 * 30);
//                //设置路径为短链接。否则cookie认为访问同一个域名下所有的短链接都已经登陆了
//                uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
//                ((HttpServletResponse) response).addCookie(uvCookie);
//                uvFirstFlag.set(Boolean.TRUE);
//                stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uvStr);
//            };
//            if (ArrayUtil.isNotEmpty(cookies)){
//                Arrays.stream(cookies)
//                        .filter(each -> Objects.equals(each.getName(), "uv"))
//                        .findFirst()
//                        .map(Cookie::getValue)
//                        .ifPresentOrElse(each -> {
//                            uv.set(each);
//                            Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
//                            uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
//                        }, addResponseCookieTask);
//            }else {
//                addResponseCookieTask.run();
//            }
//
//            //统计uip
//            String actualIp = LinkUtil.getActualIp((HttpServletRequest) request);
//            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, actualIp);
//            boolean uipFirstFlag = (uipAdded != null && uipAdded > 0L);
//
//            if (gid == null){
//                LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
//                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
//                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
//                gid = shortLinkGotoDO.getGid();
//            }
//
//            //统计小时和星期
//            int hour = DateUtil.hour(new Date(), true);//获得指定日期的小时数部分
//            Week week = DateUtil.dayOfWeekEnum(new Date());//获得指定日期是星期几
//            int weekValue = week.getIso8601Value();
//
//            //创建并插入短链接基础访问监控
//            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
//                    .fullShortUrl(fullShortUrl)
//                    .gid(gid)
//                    .date(new Date())
//                    .hour(hour)
//                    .weekday(weekValue)
//                    .pv(1)
//                    .uv(uvFirstFlag.get() ? 1:0)
//                    .uip(uipFirstFlag ? 1:0)
//                    .build();
//            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
//
//            //统计地区信息
//            //使用高德的api：restapi.amap.com/v3/ip?key=您的key&ip=114.247.50.2
//            Map<String, Object> localeMap = new HashMap<>();
//            localeMap.put("key", statsLocaleAmapKey);
//            localeMap.put("ip", actualIp);
//            String localeResult = HttpUtil.get(ShortLinkConstant.AMAP_REMOTE_URL, localeMap);
//            JSONObject jsonObject = JSON.parseObject(localeResult);
//            String infcode = jsonObject.getString("infocode");
//            String actualProvince = "未知";
//            String actualCity = "未知";
//            if (StrUtil.isNotBlank(infcode) && StrUtil.equals(infcode, "10000")){ //10000代表正确响应码
//                //province为[]，其他也为[]
//                String province = jsonObject.getString("province");
//                boolean provinceFlag = StrUtil.equals(province, "[]");
//                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
//                        .fullShortUrl(fullShortUrl)
//                        .gid(gid)
//                        .date(new Date())
//                        .cnt(1)
//                        .province(actualProvince = provinceFlag ? "未知":province)
//                        .city(actualCity = provinceFlag ? "未知":jsonObject.getString("city"))
//                        .adcode(provinceFlag ? "未知":jsonObject.getString("adcode"))
//                        .country("中国")
//                        .build();
//                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
//            }
//
//            //统计操作系统信息
//            String os = LinkUtil.getOs((HttpServletRequest) request);
//            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
//                    .fullShortUrl(fullShortUrl)
//                    .gid(gid)
//                    .date(new Date())
//                    .cnt(1)
//                    .os(os)
//                    .build();
//            linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
//
//            //统计浏览器信息
//            String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
//            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
//                    .fullShortUrl(fullShortUrl)
//                    .gid(gid)
//                    .date(new Date())
//                    .cnt(1)
//                    .browser(browser)
//                    .build();
//            linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
//
//            //统计设备信息
//            String device = LinkUtil.getDevice(((HttpServletRequest) request));
//            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
//                    .fullShortUrl(fullShortUrl)
//                    .gid(gid)
//                    .date(new Date())
//                    .cnt(1)
//                    .device(device)
//                    .build();
//            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
//
//            //统计网络信息
//            String network = LinkUtil.getNetwork(((HttpServletRequest) request));
//            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
//                    .gid(gid)
//                    .fullShortUrl(fullShortUrl)
//                    .date(new Date())
//                    .cnt(1)
//                    .network(network)
//                    .build();
//            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
//
//            //统计日志
//            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
//                    .fullShortUrl(fullShortUrl)
//                    .gid(gid)
//                    .user(uv.get())
//                    .ip(actualIp)
//                    .browser(browser)
//                    .os(os)
//                    .device(device)
//                    .network(network)
//                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
//                    .build();
//            linkAccessLogsMapper.insert(linkAccessLogsDO);
//
//            //短链接访问统计自增
//            baseMapper.incrementStats(gid, fullShortUrl, 1, uvFirstFlag.get() ? 1:0, uipFirstFlag ? 1:0);
//
//            //统计当天短链接
//            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
//                    .gid(gid)
//                    .fullShortUrl(fullShortUrl)
//                    .date(new Date())
//                    .todayPv(1)
//                    .todayUv(uvFirstFlag.get() ? 1:0)
//                    .todayUip(uipFirstFlag ? 1:0)
//                    .build();
//            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
//        } catch (Throwable e) {
//            throw new ServiceException("短链接访问统计异常！");
//        }
//    }

    /**
     * 批量创建短链接
     */
    @Transactional
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    /**
     * 构建监控的基本信息
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();//该用户是否是第一次访问
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();//为了在后面可以拿到uv

        //统计uv
        Runnable addResponseCookieTask = () -> {
            String uvStr = UUID.fastUUID().toString();
            uv.set(uvStr);
            Cookie uvCookie = new Cookie("uv", uvStr);
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            //设置路径为短链接。否则cookie认为访问同一个域名下所有的短链接都已经登陆了
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uvStr);
        };
        if (ArrayUtil.isNotEmpty(cookies)){
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        }else {
            addResponseCookieTask.run();
        }

        //统计uip
        String actualIp = LinkUtil.getActualIp((HttpServletRequest) request);
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, actualIp);
        boolean uipFirstFlag = (uipAdded != null && uipAdded > 0L);

        //统计操作系统
        String os = LinkUtil.getOs((HttpServletRequest) request);
        //统计浏览器
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        //统计设备
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        //统计网络
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));

        //使用ShortLinkStatsRecordDTO来封装种统计数据
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(actualIp)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }

    /**
     * 短链接状态监控
     * 这里是使用消息队列重构后的
     */
    @Override
    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecord) {
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("fullShortUrl", fullShortUrl);
        producerMap.put("gid", gid);
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        //redis stream
        // shortLinkStatsSaveProducer.send(producerMap);
        //rabbitmq
        // rabbitMqStatsProducer.send(producerMap);
        useMqFactory.getMqTypes().sendMessage(producerMap);
    }


    /**
     * 验证短链接是否合法
     */
    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        //获取原始链接中的域名：如果原始链接包含 www 开头的话需要去掉
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }

}
